import re
import curl_cffi
from bs4 import BeautifulSoup

class CommitQuery:
    """
    从 https://lore.kernel.org/linux-cve-announce 中进行commit查询
    """
    def __init__(self):
        self.base_url = "https://lore.kernel.org/linux-cve-announce/"
        self.headers = {
            "X-Requested-With": "XMLHttpRequest"
        }

    def get_cve_url(self, cve: str):
        # 查询参数
        params = {"q": cve}
        # 发送GET请求
        response = curl_cffi.get(
            self.base_url,
            params=params,
            headers=self.headers,
            verify=False
        )
        # 检查请求是否成功
        if response.status_code != 200:
            print(f"Failed to retrieve data. Status code: {response.status_code}")
            return None
        # 解析HTML内容
        soup = BeautifulSoup(response.text, 'html.parser')
        # 查找包含CVE编号的链接
        cve_link = soup.find('a', href=True, string=lambda text: text and cve in text)
        if not cve_link:
            print("CVE link not found in the response.")
            return None
        if "REJECTED" in cve_link.get_text().upper():
            return None
        # 获取CVE详情页面的相对URL
        relative_url = cve_link['href']
        # 将相对URL转换为完整URL
        cve_detail_url = self.base_url + relative_url
        return cve_detail_url
    
    def get_all_cve(self):
        url = self.base_url
        cve_list = []
        # 定义CVE编号匹配模式（包含锚定边界）
        cve_pattern = re.compile(r'\b(CVE-\d{4}-\d+)\b')

        while url:
            response = curl_cffi.get(url, headers=self.headers, verify=False)
            soup = BeautifulSoup(response.text, 'html.parser')

            for link in soup.find_all('a', href=True):
                if "REJECTED" in link.get_text().upper():  # 跳过包含REJECTED标识的条目
                    continue
                link_text = link.text.strip()
                # 提取文本中的CVE编号
                cve_match = cve_pattern.search(link_text)
                if cve_match:
                    cve_number = cve_match.group(1).upper()  # 统一转为大写
                    cve_list.append(cve_number)
            # 分页处理
            next_link = soup.find('a', string='next (older)')
            url = self.base_url + next_link['href'] if next_link else None
            # 去重并保持顺序（Python 3.7+）
        return list(dict.fromkeys(cve_list))

    def get_all_cve_url(self):
        url = self.base_url
        cve_url_list = []
        while url:
            response = curl_cffi.get(url, headers=self.headers, verify=False)
            soup = BeautifulSoup(response.text, 'html.parser')
            
            # Find all CVE links
            for link in soup.find_all('a', href=True):
                if 'CVE' in link.text:
                    relative_url = link['href']
                    cve_url = self.base_url + relative_url
                    cve_url_list.append(cve_url)
                    # You can add code here to visit the CVE link and process its content
            next_link = soup.find('a', string='next (older)')
            if next_link:
                url = self.base_url + next_link['href']
            else:
                url = None
        return cve_url_list

    def _get_commit_msg(self, url: str):
        # 发送请求获取CVE详情页面内容
        detail_response = curl_cffi.get(url, headers=self.headers, verify=False)
        if detail_response.status_code != 200:
            print(f"Failed to retrieve CVE detail page. Status code: {detail_response.status_code}")
            return None
        # 解析CVE详情页面内容
        detail_soup = BeautifulSoup(detail_response.text, 'html.parser')
        return detail_soup.get_text()
    
    def _get_commit_info_from_msg(self, msg: str) -> list[dict]:
        if not msg:
            return None
        commits_info = []
        lines = msg.split('\n')
        for line in lines:
            introduced_version = None
            introduced_commit = None
            fixed_version = None
            fixed_commit = None
            issue_number = None

            if "introduced in" not in line and "fixed in" not in line and "Fixed in" not in line:
                continue
            line = line.strip()
            pattern = re.compile(r"Issue introduced in ([a-zA-Z0-9-.]+) with commit ([a-f0-9]+) and fixed in ([a-zA-Z0-9-.]+) with commit ([a-f0-9]+)")
            matches = pattern.findall(line)
            if len(matches) != 0:
                introduced_version, introduced_commit, fixed_version, fixed_commit = matches[0]
            else:
                pattern = re.compile(r"Issue introduced in ([a-zA-Z0-9-.]+) with commit ([a-f0-9]+)")
                matches = pattern.findall(line)
                if len(matches) != 0:
                    introduced_version, introduced_commit = matches[0]
                else:
                    pattern = re.compile(r"Fixed in ([a-zA-Z0-9-.]+) with commit ([a-f0-9]+)")
                    matches = pattern.findall(line)
                    if len(matches) != 0:
                        fixed_version, fixed_commit = matches[0]
                    else:
                        print(f'The line: {line} cannot be resolved')

            # 尝试从行文本中提取issue编号
            issue_match = re.search(r'(?:#|GH-)(\d+)', line)
            if issue_match:
                issue_number = issue_match.group(1)
            
            commit_info = {
                "introduced_version": introduced_version,
                "introduced_commit": introduced_commit,
                "fixed_version": fixed_version,
                "fixed_commit": fixed_commit,
                "issue_number": issue_number
            }
            commits_info.append(commit_info)
        return commits_info
    
    def get_commit_infos(self, cve: str) -> list[dict]:
        url = self.get_cve_url(cve)
        if url:
            msg = self._get_commit_msg(url)
            if msg:
                commit_infos = self._get_commit_info_from_msg(msg)
                return commit_infos
        return []
    
    def get_introduced_commit(self, commits_info: list[dict]) -> dict:
        introduced_commit_info = {}
        for commit_info in commits_info:
            version = commit_info.get('introduced_version')
            commit = commit_info.get('introduced_commit')
            if version is not None and commit is not None:
                introduced_commit_info[version] = commit
        return introduced_commit_info
    
    def get_fixed_commit(self, commits_info: list[dict]) -> dict:
        fixed_commit_info = {}
        for commit_info in commits_info:
            version = commit_info.get('fixed_version')
            commit = commit_info.get('fixed_commit')
            if version is not None and commit is not None:
                fixed_commit_info[version] = commit
        return fixed_commit_info


